<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
	<title>jquery.datastore.js Test Suite</title>
	<link rel="stylesheet" href="qunit.css" type="text/css"/>
	<script type="text/javascript" src="jquery-1.4.1.min.js"></script>
	<script type="text/javascript" src="jquery.datastore.js"></script> 	 
    <script type="text/javascript" src="qunit.js"></script>
    <script type="text/javascript">
	$(function() {
		var error_ok = function(callback, message) {
			try {
				callback.apply(null);
			}
			catch(e) {
				console.info("Error OK: " + e);
				ok(true, message);
				return;
			}
			ok(false, message);
		};
		
		module("Core Developer Interface");
		
		test("Transactions", function() {
			root = new $.ds.node();
			
			root.add("foo");
			root.add("bar");
			same(root.subpaths(), ["foo", "bar"], "Paths Created");
			root.revert();
			same(root.subpaths(), [], "Paths Reverted");
			root.add("foo");
			root.add("bar");
			root.commit();
			root.revert();
			same(root.subpaths(), ["foo", "bar"], "Paths Committed");
			
			root.add("gaz");
			root.del("gaz");
			ok(root.add("gaz"), "Adding a deleted path");
		});
		
		test("Value Nodes", function() {
			root = new $.ds.node();
			
			ok(root.add("value", 1).kind() == 'value', "Value Kind");
			error_ok(function()  {
				root.add("value.value", 1);
			}, "Value Subnode Fail");
			
			root.commit();
			ok(root.get("value").value() == 1, "Value Committed");
			root.get("value").value(2);
			ok(root.get("value").value() == 2, "Value Changed");
			root.revert();
			same(root.get("value").value(), 1, "Value Reverted");
		});
		
		test("Array Nodes", function() {
			root = new $.ds.node();
			
			ok(root.add("array", []).kind() == 'array', "Array Kind");
			error_ok(function()  {
				root.add("array.value", 1);
			}, "Array Non-Digit Subnode");
			ok(root.add("array.0", []), "Array Subnode OK");
			same(root.get("array").add(1, 1).value(), 1, "Number Subnode");
			error_ok(function()  {
				root.add("array.3", 1);
			}, "Array Out of Bounds Subnode");
			error_ok(function()  {
				root.get("array").add(-1, 1);
			}, "Array Negative Subnode");
			error_ok(function()  {
				root.get("array").value({});
			}, "Invalid array value");
			
			root.del("array");
			error_ok( function() {
				root.add("array.1", 1)
			}, 'Kind Change Via Re-Initialization (Array -> Object)');
		});
		
		test("Object Nodes", function() {
			root = new $.ds.node();
			
			ok(root.add("object", {}).kind() == 'object', "Object Kind");
			ok(root.add("object.value", {}).kind() == 'object', "Object Subnode OK");
			
			error_ok(function()  {
				root.get("object").value([]);
			}, "Invalid object value");
			
			var nested = {
				value : 1,
				object : {
					a : "a",
					b : "b"
				},
				array : [0,1,2]
			};
			same(root.add("nested", nested).value(), nested, "Nesting");
			
			same(root.get("nested.value").kind(), 'value', "Nested Value Kind");
			same(root.get("nested.object").kind(), 'object', "Nested Object Kind");
			same(root.get("nested.array").kind(), 'array', "Nested Array Kind");
		});
		
		test("Calculated Nodes", function() {
			root = new $.ds.node();
			
			error_ok(function() {
				root.add("calculated_missing", function() {});
			}, "Calculated Kind, Missing Keys");
			error_ok(function() {
				root.add("calculated_missing", function() {}, 1);
			}, "Calculated Kind, Bad Keys");
			ok(root.add("calculated", function() {}, []).kind() == 'calculated', "Calculated Kind");
			error_ok(function() {
				root.get("calculated").value(1, 1);
			}, "Calculated Kind, Bad Calculator");
			error_ok(function()  {
				root.add("calculated.value", 1);
			}, "Calculated Subnode Fail");
			
			root = new $.ds.node();
			root.add("a", "foo");
			root.add("b", "bar");
			root.add("calc", function(a, b) {
				return a + b;
			}, ["a", "b"]);
			ok(root.get("calc").value() == "foobar", "Calculated Value");
			root.commit();
			ok(root.get("calc").value() == "foobar", "Calculator Committed");
			root.get("calc").value(function(b, a) {
				return b + a;
			}, ["b", "a"]);
			ok(root.get("calc").value() == "barfoo", "Calculator Changed");
			root.revert();
			ok(root.get("calc").value() == "foobar", "Calculator Reverted");
			
			root.get("calc").value(function(b, a) {
				return b + a;
			}, ["b", "c"]);
			error_ok(function() {
				root.get("calc").value()
			}, "Calculator Missing Key");
			
			root.add("sub.path.calc", function(a,b) {
				return a + b;
			}, ["../../../a", "../../../b"]);
			ok(root.get("sub.path.calc").value() == "foobar", "Relative Key Paths");
			
			root.get("sub.path.calc").value(function(a,b) {
				return a + b;
			}, ["../../../a", "../../b"]);
			error_ok(function() {
				root.get("sub.path.calc").value()
			}, "Missing Relative Key Paths");
		});
		
		test("Metadata Nodes - Builtin", function() {
			root = new $.ds.node();
			root.add("value", 1);
			
			same( root.get("value.@path").value(), root.get("value").path(), "Metadata Item" );
			same( root.get("value").metadata(), { 
				"@id" : root.get("value").id(), 
				"@kind" : root.get("value").kind(),
				"@name" : root.get("value").name(),
				"@subnames" : root.get("value").subnames(),
				"@path" : root.get("value").path(),
				"@subpaths" : root.get("value").subpaths()
			}, "Metadata Object");
			same( root.get("value").metapaths().sort(), ["value.@id", 'value.@kind', "value.@name", "value.@path", "value.@subnames", "value.@subpaths"], "Value Metapaths" );
			same( root.get("value").metanames().sort(), ["@id", '@kind', "@name", "@path", "@subnames", "@subpaths"], "Value Metanames" );
		});
		
		test("Relocating Nodes", function() {
			root = new $.ds.node();
			
			root.add("foo", 1);
			root.add("bar", 2);
			root.commit();
			
			root.get("bar").rename("baz");
			same(root.subnames().sort(), ["baz", "foo"], "Renamed");
			
			root.revert();
			same(root.subnames().sort(), ["bar", "foo"], "Rename Reverted");
			
			root.get("bar").rename("baz");
			root.commit();
			same(root.subnames().sort(), ["baz", "foo"], "Rename Committed");
			
			root.get("baz").rename("bar_0");
			root.get("bar_0").rename("bar_1");
			root.revert();
			same(root.subnames().sort(), ["baz", "foo"], "Multiple Rename Reverted");
			
			root.get("baz").rename("bar_0");
			root.get("bar_0").rename("bar_1");
			root.commit();
			same(root.subnames().sort(), ["bar_1", "foo"], "Multiple Rename Committed");
			
			root = new $.ds.node();
			root.add("a", "a");
			root.add("b", "b");
			root.commit();
			
			root.get("a").rename("temp");
			root.get("b").rename("a");
			root.get("temp").rename("b");
			
			same(root.value(), { a: "b", b: "a"}, "Swap Via Rename.");
			root.revert();
			
			same(root.value(), { a: "a", b: "b"}, "Swap Reverted.");
			
			root.get("a").rename("temp");
			root.get("b").rename("a");
			root.get("temp").rename("b");
			root.commit();
			same(root.value(), { a: "b", b: "a"}, "Swap Committed.");
			
			root = new $.ds.node();
			
			root.add("array", [0, 1, 2, 3, 4]);
			root.get("array.3").rename("2");
			root.get("array.4").rename("3");
			same(root.get("array").value(), [0,1,3,4], "Array RemoveAt(2) Via Rename");
			root.revert();
			
			root.add("array", [0, 1, 3, 4, 5]);
			root.get("array.4").rename("5");
			root.get("array.3").rename("4");
			root.get("array.2").rename("3");
			root.add("array.2", 2);
			same(root.get("array").value(), [0,1,2,3,4,5], "Array InsertAt(2) Via Rename");
			root.revert();
			
			root = new $.ds.node();
			root.add("object_1.array", [0,1,2,3]);
			root.add("object_2", {});
			root.commit();
			
			root.get("object_1.array").moveTo("object_2.array");
			same(root.get("object_2.array").value(), [0,1,2,3], "Relocating");
			
			root.revert();
			same(root.value(), { object_1 : { array : [0,1,2,3] }, object_2 : {} }, "Relocation Reverted");
		});
		
		module("Storage");
		
		test("Transactions", function() {
			$.ds.loadModule("storage");
			
			error_ok(function() {
				$.ds.storage.setValue("foo", "bar");
			}, "Non-Transactional Mutation");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.setValue("foo", "bar");
				ok($.ds.storage.changeset["foo"], "Changeset - Mutation");
			});
			ok( $.ds.storage.getValue("foo") == "bar", "Transactional Mutation" );
			
			error_ok(function() {
				$.ds.storage.deleteValue("foo");
			}, "Non-Transactional Deletion");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.deleteValue("foo");
				ok($.ds.storage.changeset["foo"], "Changeset - Deletion");
			});
			ok( $.ds.storage.hasValue("foo") == false, "Transactional Deletion");
			
			$.ds.unloadModule("storage");
		});
		
		test("Caching", function() {
			$.ds.loadModule("storage");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.setValue("foo", "foo");
				$.ds.storage.setValue("bar", "bar");
				$.ds.storage.setValue("foobar", function(a, b) {
					return a + b;
				}, ["../foo", "../bar"]);
			});
			
			$.ds.storage.getValue("foobar");
			ok($.ds.storage.cache["foobar"], "Value Set In Cache");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.setValue("bar", "world");
				ok($.ds.storage.cache["foobar"] === undefined, "Value Cleared From Cache");
			});
			
			$.ds.storage.transaction(function() {
				$.ds.storage.deleteValue("foobar");
			});
			same( $.ds.storage.getDependents("foo"), [], "Remove Dependents");
			
			$.ds.unloadModule("storage");
			
			$.ds.loadModule("storage");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.setValue("foo", "foo");
				$.ds.storage.setValue("bar", "bar");
				$.ds.storage.setValue("baz", "baz");
				$.ds.storage.setValue("foobar", function(a, b) {
					return a + b;
				}, ["../foo", "../bar"]);
			});
			$.ds.storage.getValue("foobar"); //Set the cache
			
			$.ds.storage.transaction(function() {
				$.ds.storage.root.get("baz").rename("bar");
			});
			same($.ds.storage.getValue("foobar"), "foobaz", "Rename Clears Cache");
			
			$.ds.unloadModule("storage");
		});
		
		module("Query");
		
		test("Queries", function() {
			
			$.ds.loadModule("query");
			root = new $.ds.node();
			root.add("books", [
				{ title: "The Dream Songs", cost: 1000, "@bookid" : 12, "@foo" : "bar" },
				{ title: "Jaka's Story", cost: 845, "@bookid" : 13, "@foo" : "bar" },
				{ title: "Duchamp", cost: 492, "@bookid" : 14, "@foo" : "bar" },
				{ title: "Burning Chrome", cost: 1295, "@bookid" : 15, "@foo" : "bar" }
			]);
			
			
			var nodes = $.ds.query.execute(root, "books.*[cost > $min_cost, cost < $max_cost].title", { min_cost: 800, max_cost: 1100 });
			same(nodes, [root.get("books.0.title"), root.get("books.1.title")], "Valid Query");
			
			var nodes = $.ds.query.execute(root, "foobar");
			same(nodes, [], "Empty Query");
			
			var nodes = $.ds.query.execute(root, "books.*[@bookid = $book]", {book:13});
			same(nodes, [root.get("books.1")], "Metadata Query");
			
			var nodes = $.ds.query.execute(root, "books.0.@*");
			var i = 0;
			if(
				nodes.length == root.get("books.0").metanames().length && 
				nodes[i++] == root.get("books.0.@bookid") &&
				nodes[i++] == root.get("books.0.@foo") &&
				nodes[i++] == root.get("books.0.@id") &&
				nodes[i++] == root.get("books.0.@kind") &&
				nodes[i++] == root.get("books.0.@path") &&
				nodes[i++] == root.get("books.0.@subpaths") &&
				nodes[i++] == root.get("books.0.@name") &&
				nodes[i++] == root.get("books.0.@subnames")
			) {
				ok(true, "Metadata Star Accessor");
			}
			else {
				ok(false, "Metadata Star Accessor @ " + i);
			}
			
			
			error_ok(function() {
				$.ds.query.execute();
			}, "Bad Arguments, No Node.");
			
			error_ok(function() {
				$.ds.query.execute(null);
			}, "Bad Arguments, Null Node.");
			
			error_ok(function() {
				$.ds.query.execute(root, []);
			}, "Bad Arguments, Non-String Statment.");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost > $min_cost", { min_cost: 800 });
			}, "Missing Bracket");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[ > $min_cost]", { min_cost: 800 });
			}, "Missing Condition Path");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost $min_cost]", { min_cost: 800 });
			}, "Missing Condition Operator");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost foo $min_cost]", { min_cost: 800 });
			}, "Undefined Condition Operator");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost < $min_cost]");
			}, "Missing Parameters");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost < ]", { min_cost: 800 });
			}, "Missing Condition Parameter");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost < min_cost]", { min_cost: 800 });
			}, "Invalid Condition Parameter");
			
			error_ok(function() {
				$.ds.query.execute(root, "books.*[cost < $foobar]", { min_cost: 800 });
			}, "Undefined Condition Parameter");
			
			$.ds.unloadModule("query");
		});
		
		module("Core User Interface");
		
		test("Each & One", function() {
			$.ds.loadModule("storage");
			$.ds.loadModule("query");
			
			$.ds.storage.transaction(function() {
				$.ds.storage.setValue("books", [
					{ title: "The Dream Songs", cost: 1000 },
					{ title: "Jaka's Story", cost: 845 },
					{ title: "Duchamp", cost: 492 },
					{ title: "Burning Chrome", cost: 1295 }
				]);
			});
			
			var results = [];
			$.ds("books.*.title").each(function(node, index) {
				results.push( node.value() );
			});
			same(results, ["The Dream Songs", "Jaka's Story", "Duchamp", "Burning Chrome"], "Each");
			
			var results = [];
			$.ds("books.*[cost > $min_cost, cost < $max_cost].title", { min_cost: 800, max_cost: 1100 }).each(
				function(node, index) {
					results.push( node.value() );
				}
			);
			same(results, ["The Dream Songs", "Jaka's Story"], "Each With Params");
			
			var result = null;
			$.ds("books.*.title").one(function(node, index) {
				result = node.value();
			});
			same(result, "The Dream Songs", "One");
			
			$.ds.unloadModule("storage");
			$.ds.unloadModule("query");
		});
		
		module("Common");
		
		test("Value, Remove, Insert", function() {
			$.ds.loadModule("storage");
			$.ds.loadModule("query");
			$.ds.loadModule("common");
			
			same( $.ds("foo").value(), undefined, "Unset Node Value");
			same( $.ds("foo").value("bar").value(), "bar", "Set String Node Value");
			same( $.ds("foo").value("baz").value(), "baz", "Overwrite String Node Value");
			
			same( $.ds("obj").value({foo:"foo", bar:"bar"}).value(), {foo:"foo", bar:"bar"}, "Set Object Node Value");
			same( $.ds("obj.foo").value(), "foo", "Get One Object Subnode Value");
			same( $.ds("obj.*").value(), ["foo", "bar"], "Get Many Subnode Values");
			
			same ($.ds("ary").value([1, 2, 3]).value(), [1,2,3], "Set Array Node Value");
			same( $.ds("ary.1").value(), 2, "Get One Array Subnode Value");
			same( $.ds("ary.*").value(), [1,2,3], "Get Many Array Values");
			
			$.ds("calc").value( function(a, b) { return a + b; }, ["obj.foo", "obj.bar"] );
			same( $.ds("calc").value(), "foobar", "Set Calculated Value");
			$.ds("obj.bar").value("world");
			same( $.ds("calc").value(), "fooworld", "Verify Calculated Value");
			
			error_ok(function() {
				$.ds("calc").value( function() { return a, b } );
			}, "Calculated Value Error Check")
			
			
			same( $.ds("foo.@kind").value(), "value", "Metadata Access");
			
			same( $.ds("foo").remove().value(), undefined, "Remove Value" );
			
			same( $.ds("ary").remove(0).value(), [2,3], "Remove At Index");
			same( $.ds("ary").remove("*").value(), [], "Remove All");
			
			same( $.ds("ary").insert(1).value(), [1], "Array Insert");
			same( $.ds("ary").insert(0, "foo").value(), ["foo", 1], "Array Insert At Index");
			error_ok(function() {
				$.ds("ary").insert(10, "foo");
			}, "Array Bounds Sanity Check");
			
			same( $.ds("obj").insert("hello", "world").value(), { foo:"foo", bar:"world", hello:"world"}, "Object Insert");
			error_ok(function() {
				$.ds("obj").insert("x");
			}, "Object Key Sanity Check");
			
			$.ds("obj").insert("@crap", {});
			same( $.ds("obj.@crap").value(), {}, "Metadata Insert");
			
			$.ds.unloadModule("common");
			$.ds.unloadModule("query");
			$.ds.unloadModule("storage");
		});
		
		module("Validators");
		
		test("Global", function() {
			$.ds.loadModule("storage");
			$.ds.loadModule("query");
			$.ds.loadModule("validators");
			
			var email = function(value) {
				var emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}$/;  
				if( ! emailPattern.test(value) )
					return "'" + value + "' is not a well-formed email address.";
			}
			
			$.ds.validators.add("email", email);
			same( $.ds.validators.has("email"), true, "Has Validator");
			same( $.ds.validators.get("email").toString(), email.toString(), "Get Validator");
			
			same( $.ds.validators.has("foo"), false, "Missing Validator");
			
			error_ok( function() {
				$.ds.validators.get("foo");
			}, "Get Missing Validator");
			
			error_ok( function() {
				$.ds.validators.add("foo", "bar");
			}, "Add Bad Validator");
			
			$.ds.unloadModule("storage");
			$.ds.unloadModule("query");
			$.ds.unloadModule("validators");
		});
		
		test("User Interface", function() {
			$.ds.loadModule("storage");
			$.ds.loadModule("query");
			$.ds.loadModule("common");
			$.ds.loadModule("validators");
			
			$.ds("email").value("foobar");
			
			same( $.ds("email").valid(), true, "Unvalidated Node OK" );
			error_ok(function() {
				$.ds("email").valid("foo");
			}, "Missing Validator Sanity Check");
			
			var email = function(value) {
				var emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}$/;  
				if( ! emailPattern.test(value) )
					return "'" + value + "' is not a well-formed email address.";
			}
			
			$.ds.validators.add("email", email);
			$.ds("email").valid("email");
			same( $.ds("email.@validation.tests").value(), ["email"], "Validator Attached" );
			same( $.ds("email").valid(), false, "Auto Validation - Setting Validators" );
			same( $.ds("email").value("roarkely@gmail.com").valid(), true, "Auto Validation - Setting Values");
			
			$.ds.unloadModule("storage");
			$.ds.unloadModule("query");
			$.ds.unloadModule("common");
			$.ds.unloadModule("validators");
		});
	});
    </script>
	<style type="text/css" media="screen">
		.collection {
			width:40%;
			margin-left:5%;
			float:left;
		}
	</style>
  </head>
  <body>
    <h1 id="qunit-header">jquery.datastore.js Test Suite</h1>
    <h2 id="qunit-banner"></h2>
    <h2 id="qunit-userAgent"></h2>
    <ol id="qunit-tests">
    </ol>
  </body>
</html>